﻿
Imports System.Collections.Generic
Imports System.Text
Imports System.Windows.Forms
Imports System.IO


Namespace clsFormToMultipartPostData
    '*
    '     * 
    '     * This is a helper class to build postData header an body with the values from 
    '     * the given HTML form. The purpose of this class is to attach files to 
    '     * the <INPUT TYPE="FILE"> fields.
    '     * 
    '     * License: Code Project Open License (CPOL)
    '     * (c) Kirill Hryapin, 2008
    '     * (c) Steven Cheng (Microsoft), 2005 (?)
    '     * 
    '     * See usage example in Form1.cs included with this distribution.
    '     * 
    '     


    Public Class clsFormToMultipartPostData
        Private Structure ValuePair
            ' KeyValuePair<string, string> sounds too verbose for me
            Public name As String
            Public value As String
            Public Sub New(ByVal name As String, ByVal value As String)
                Me.name = name
                Me.value = value
            End Sub
        End Structure

        Public Structure RequestParameters
            Public data As Byte()
            Public headers As String
        End Structure

        Private values As New List(Of ValuePair)()
        Private files As New List(Of ValuePair)()
        Private overloadedFiles As New Dictionary(Of String, String)()

        Private form As HtmlElement
        Private webbrowser As WebBrowser

        '*
        '         * In most circumstances, this constructor is better (allows to use Submit() method)
        '         

        Public Sub New(ByVal b As WebBrowser, ByVal f As HtmlElement)
            form = f
            webbrowser = b
            GetValuesFromForm(f)
        End Sub

        '*
        '         * Use this constructor if you don't want to use Submit() method
        '         

        Public Sub New(ByVal f As HtmlElement)
            GetValuesFromForm(f)
        End Sub

        '*
        '         * Submit the form
        '         

        Public Sub Submit()
            Dim url As New Uri(webbrowser.Url, form.GetAttribute("action"))
            Dim req As RequestParameters = GetEncodedPostData()
            webbrowser.Navigate(url, form.GetAttribute("target"), req.data, req.headers)
        End Sub

        '*
        '         * Load values from form
        '         

        Private Sub GetValuesFromForm(ByVal form As HtmlElement)
            ' Get values from the form
            For Each child As HtmlElement In form.All
                Select Case child.TagName
                    Case "INPUT"
                        Select Case child.GetAttribute("type").ToUpper()
                            Case "FILE"
                                AddFile(child.Name, child.GetAttribute("value"))
                                Exit Select
                            Case "CHECKBOX", "RADIO"
                                If child.GetAttribute("checked") = "True" Then
                                    AddValue(child.Name, child.GetAttribute("value"))
                                End If
                                Exit Select
                            Case "BUTTON", "IMAGE", "RESET"
                                Exit Select
                            Case Else
                                ' Ignore those?
                                AddValue(child.Name, child.GetAttribute("value"))
                                Exit Select
                        End Select
                        Exit Select
                    Case "TEXTAREA", "SELECT"
                        ' it's legal in IE to use .value with select (at least in IE versions 3 to 7)
                        AddValue(child.Name, child.GetAttribute("value"))
                        Exit Select
                        ' of "switch tagName"
                End Select
            Next
            ' of "foreach form child"
        End Sub

        Private Sub AddValue(ByVal name As String, ByVal value As String)
            If name = "" Then
                Return
            End If
            ' e.g. unnamed buttons
            values.Add(New ValuePair(name, value))
        End Sub

        Private Sub AddFile(ByVal name As String, ByVal value As String)
            If name = "" Then
                Return
            End If
            files.Add(New ValuePair(name, value))
        End Sub

        '*
        '         * Set file field value [the reason why this class exist]
        '         

        Public Sub SetFile(ByVal fieldName As String, ByVal filePath As String)
            Me.overloadedFiles.Add(fieldName, filePath)
        End Sub

        '*
        '         * One may need it to know whether there's specific file input
        '         * For example, to perform some actions (think format conversion) before uploading
        '         

        Public Function HasFileField(ByVal fieldName As String) As Boolean
            For Each v As ValuePair In files
                If v.name = fieldName Then
                    Return True
                End If
            Next
            Return False
        End Function

        '*
        '         * Encode parameters 
        '         * Based on the code by Steven Cheng, http://bytes.com/forum/thread268661.html
        '         

        Public Function GetEncodedPostData() As RequestParameters
            Dim boundary As String = "----------------------------" + DateTime.Now.Ticks.ToString("x")

            Dim memStream As Stream = New System.IO.MemoryStream()
            Dim boundarybytes As Byte() = System.Text.Encoding.ASCII.GetBytes((Convert.ToString(vbCr & vbLf & "--") & boundary) + vbCr & vbLf)

            Dim formdataTemplate As String = (Convert.ToString(vbCr & vbLf & "--") & boundary) + vbCr & vbLf & "Content-Disposition: form-data; name=""{0}"";" & vbCr & vbLf & vbCr & vbLf & "{1}"
            For Each v As ValuePair In values
                Dim formitem As String = String.Format(formdataTemplate, v.name, v.value)
                Dim formitembytes As Byte() = System.Text.Encoding.UTF8.GetBytes(formitem)
                memStream.Write(formitembytes, 0, formitembytes.Length)
            Next
            memStream.Write(boundarybytes, 0, boundarybytes.Length)

            Dim headerTemplate As String = "Content-Disposition: form-data; name=""{0}""; filename=""{1}""" & vbCr & vbLf & "Content-Type: application/octet-stream" & vbCr & vbLf & vbCr & vbLf

            For Each v As ValuePair In files
                Dim filePath As String

                If overloadedFiles.ContainsKey(v.name) Then
                    filePath = overloadedFiles(v.name)
                Else
                    If v.value.Length = 0 Then
                        Continue For
                    End If
                    ' no file
                    filePath = v.value
                End If

                Try
                    ' file can be absent or not readable
                    Dim fileStream As New FileStream(filePath, FileMode.Open, FileAccess.Read)

                    Dim header As String = String.Format(headerTemplate, v.name, filePath)
                    Dim headerbytes As Byte() = System.Text.Encoding.UTF8.GetBytes(header)
                    memStream.Write(headerbytes, 0, headerbytes.Length)

                    Dim buffer As Byte() = New Byte(1023) {}
                    Dim bytesRead As Integer = 0
                    While (InlineAssignHelper(bytesRead, fileStream.Read(buffer, 0, buffer.Length))) <> 0
                        memStream.Write(buffer, 0, bytesRead)
                    End While

                    memStream.Write(boundarybytes, 0, boundarybytes.Length)
                    fileStream.Close()
                Catch x As Exception
                    ' no file?..
                    MessageBox.Show(x.Message, "Cannot upload the file", MessageBoxButtons.OK, MessageBoxIcon.Warning)
                End Try
            Next

            Dim result As New RequestParameters()

            memStream.Position = 0
            result.data = New Byte(memStream.Length - 1) {}
            memStream.Read(result.data, 0, result.data.Length)
            memStream.Close()

            result.headers = (Convert.ToString("Content-Type: multipart/form-data; boundary=") & boundary) + vbCr & vbLf + "Content-Length: " + result.data.Length + vbCr & vbLf + vbCr & vbLf

            Return result
        End Function
        Private Shared Function InlineAssignHelper(Of T)(ByRef target As T, ByVal value As T) As T
            target = value
            Return value
        End Function
    End Class
End Namespace
